None
Загрузка данных и подготовка их к анализу
Анализ данных
id — идентификатор объекта;object_name — название объекта общественного питания;chain — сетевой ресторан;object_type — тип объекта общественного питания;address — адрес;number — количество посадочных мест.import pandas as pd
import numpy as np
#Подгружаю библиотеку для форматирования меток на осях
from matplotlib.ticker import PercentFormatter
import seaborn as sns
from matplotlib import pyplot as plt
import plotly.express as px
from plotly import graph_objects as go
#Для загрузки внешних данных
from io import BytesIO
import requests
Загрузка данных и обзор
#загрузка с обработкой ошибок
try:
fastfood = pd.read_csv('/datasets/rest_data.csv')
except:
fastfood = pd.read_csv('pr9/rest_data.csv')
print('Общий вид данных: ')
display(fastfood.sort_values('object_name').head(5))
fastfood.info()
print('--------------------------------------------------')
print('Полных дубликатов: ', fastfood.duplicated().sum())
Общий вид данных:
| id | object_name | chain | object_type | address | number | |
|---|---|---|---|---|---|---|
| 7170 | 28685 | 1-ЫЙ МОСКОВСКИЙ КАДЕТСКИЙ КОРПУС | нет | столовая | город Москва, улица Вучетича, дом 30 | 260 |
| 1095 | 84529 | 1-й МОК | нет | столовая | город Москва, Стартовая улица, дом 1, корпус 1 | 100 |
| 14879 | 213667 | 100 личная столовая | нет | столовая | город Москва, Новодмитровская улица, дом 2, ко... | 30 |
| 14878 | 213840 | 100 личное кафе | нет | кафе | город Москва, Новодмитровская улица, дом 2, ко... | 45 |
| 1517 | 24520 | 1001 ночь | нет | ресторан | город Москва, Братиславская улица, дом 31, кор... | 70 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 15366 entries, 0 to 15365 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 15366 non-null int64 1 object_name 15366 non-null object 2 chain 15366 non-null object 3 object_type 15366 non-null object 4 address 15366 non-null object 5 number 15366 non-null int64 dtypes: int64(2), object(4) memory usage: 720.4+ KB -------------------------------------------------- Полных дубликатов: 0
Полных дубликатов, пропусков нет. Признак сети лучше привести к булевым значениям.
#Признак сети приводим к булевым значениям
fastfood['chain'] = fastfood['chain'] == 'да'
Посмотрим уникальные типы и названия заведений
display(fastfood['object_type'].unique())
display(fastfood['object_name'].unique())
array(['кафе', 'столовая', 'закусочная',
'предприятие быстрого обслуживания', 'ресторан', 'кафетерий',
'буфет', 'бар', 'магазин (отдел кулинарии)'], dtype=object)
array(['СМЕТАНА', 'Родник', 'Кафе «Академия»', ..., 'Бар Мята Lounge',
'Мята Lounge Октябрьская', 'Кальянная «Мята Lounge»'], dtype=object)
В названиях заведений видны неявные дубликаты
Названия типов меняем на короткие названия для лучшего отображения на графиках
fastfood['object_type'] = fastfood['object_type'].replace('предприятие быстрого обслуживания', 'стритфуд')
fastfood['object_type'] = fastfood['object_type'].replace('магазин (отдел кулинарии)', 'кулинария')
Приводим названия сетей к единообразию
chain_list_change = [
['макдоналдс','андерсон','старбакс','бургер кинг','папа джонс','сабвей','kfc','теремок','subway',
'шоколадница','прайм','cуши wok','кофикс','мята lounge','тануки','якитория','штолле','додо пицца',
'волконский','домино\'с пицца','караваевы','суши вок','му-му','крошка картошка','хлеб насущный','иль патио','милти'],
['Макдоналдс','АндерСон','Starbucks','Бургер Кинг','Папа Джонс','Subway','KFC','Теремок','Subway',
'Шоколадница','Прайм стар','Суши Wok','Cofix','Мята Lounge','Тануки','Якитория','Штолле','Додо Пицца',
'Волконский','Домино\'с Пицца','Братья Караваевы','Суши Wok','Му-Му','Крошка Картошка','Хлеб насущный','Иль Патио','Милти']]
#Функция замены названия
def chain_rename(name):
for item in range(len(chain_list_change[0])):
if chain_list_change[0][item] in name.lower():
return chain_list_change[1][item]
return name
fastfood['object_name'] = fastfood['object_name'].apply(chain_rename)
Посмотрим теперь дубликаты в паре (название-адрес)
fastfood[['object_name','address']].duplicated().sum()
226
Скорее всего это одни и те же заведения (хотя могут быть две кофейни сети в одном большом ТЦ).
Удаляем дубликаты
fastfood = fastfood[~fastfood[['object_name','address']].duplicated()].reset_index(drop=True)
#Палитра и увеличенный размер шрифта
sns.set_style("darkgrid")
sns.set(font_scale=1.3)
#Группируем по видам и считаем количество объектов в групее
dd = fastfood.groupby('object_type')['id'].count().sort_values(ascending=False)
#Строим график
fig, axes = plt.subplots()
axes = sns.barplot(y=dd.index, x=dd)
axes.set_title('Соотношение видов объектов общественного питания по количеству')
axes.set_ylabel('Тип объекта')
axes.set_xlabel('Количество')
fig.set_figheight(6)
fig.set_figwidth(10)
plt.show()
Самые популярный вид - кафе(очень расплывчатое понятие). Их в 2 раза больше столовых и в 3 раза больше ресторанов и стритфуда (предприятий быстрого обслуживания).
ff = fastfood.groupby('chain')['id'].count().sort_values(ascending=False)
fig, axes = plt.subplots()
axes.pie(ff, labels=['Одиночные', 'Сетевые'], \
autopct=lambda pct:'{:d}\n({:.0f}%)'.format(int(round(pct/100.*sum(ff))), pct),\
shadow=True, explode=(0.15, 0))
axes.axis("equal")
axes.set_title('Cоотношение сетевых и несетевых заведений по количеству')
plt.show();
Несетевые заведения составляют 80% рынка (или это сети скрываются под разными названиями?).
gg=fastfood.groupby(['object_type','chain']).count().reset_index().sort_values(by=['id','object_type'], ascending=False)
fig, ax1 = plt.subplots()
fig.set_figheight(6)
fig.set_figwidth(10)
ax1=sns.barplot(y='object_type', x='id', hue='chain', data=gg, palette="Blues")
ax1.set_title('Соотношение сетевых объектов по типам общепита')
ax1.set_xlabel('')
ax1.set_ylabel('')
plt.legend(title='Сети')
plt.show();
Среди сетевых заведений кафе естесственно больше по количеству, но стритфуд уже сравним даже по количеству. Посмотрим доли:
jj=fastfood.groupby('object_type')['chain'].mean().sort_values(ascending=False)
fig, axes = plt.subplots()
axes=sns.barplot(y=jj*100, x=jj.index, palette='flare')
axes.set_title('Доля сетевых объектов внутри типа')
axes.set_ylabel('Доля')
axes.set_xlabel('')
#Метки осей в процентах
axes.yaxis.set_major_formatter(PercentFormatter(decimals=0))
#Надписи над барами
for i, perc in enumerate(jj*100):
axes.text(i, perc+1, round(perc, 1), horizontalalignment='center', fontsize=12)
axes.set_ylim(0, 50)
plt.xticks(rotation=60)
plt.show();
Стритфуд - самый сетевой вид общепита. Кулинарии вторые по сетевому охвату - это кафе внутри супермаркетов? Рестораны, увы, тоже теряют свою индивидуальность.
hh = fastfood[fastfood['chain']].groupby('object_name').agg({'number' : ['count','mean']}).reset_index()
hh.columns = ['chain_name','chain_size', 'seats_mean']
#медианные значения размера сети и средней вместимости
chains_dim = hh.query('chain_size > 1')['chain_size'].median()
seats_dim = hh.query('chain_size > 1')['seats_mean'].median()
#Функции категорий
def chain_s(chain):
if chain > chains_dim:
return 'big'
return 'small'
def chain_f(chain):
if chain > seats_dim:
return '_thick'
return '_thin'
#Категоризация
hh['value'] = hh['chain_size'].apply(chain_s) + hh['seats_mean'].apply(chain_f)
print('Рейтинг категорий')
display(hh.query('chain_size > 1').groupby('value')['chain_name'].count().sort_values(ascending=False))
Рейтинг категорий
value small_thick 55 big_thin 52 small_thin 47 big_thick 41 Name: chain_name, dtype: int64
sns.jointplot(
x='chain_size', y='seats_mean', hue='value', data=hh.query('chain_size > 1'),
height=8, ratio=4, xlim=(-3,150), ylim=(-5,230))
#sns.jointplot(x='chain_size', y='seats_mean', data=hh, height=8, ratio=4, color='blue');
plt.show();
Преобладают сети с небольшим количеством объектов но большой вместимости.
Мы видели в предыдущем графике большое разнообразие во вместимости. Сначала посмотрим разброс в разрезе видов и сетевой принадлежности.
plt.figure(figsize=(12,8))
sns.boxplot(x='number', y='object_type', hue='chain', data=fastfood, linewidth=1, color='#3c86bb', fliersize=3)
plt.title('Разброс количества посадочных мест по типам и признаку сети')
plt.xlabel('Посадочные места')
plt.ylabel('')
plt.legend(title='Сети')
plt.show();
Видно, что для несетевых заведений характерны выбросы во всех видах. Следовательно "среднее по больнице" не так показательно как медиана. Смотрим не среднее, а медианное количество посадочных мест по видам.
ii = fastfood.groupby('object_type')['number'].median().sort_values(ascending=False)
fig, axes = plt.subplots()
axes = sns.barplot(y=ii.index, x=ii, palette='dark:#5AA')
axes.set_title('Вместимость по видам объектов общественного питания')
axes.set_ylabel('Тип объекта')
axes.set_xlabel('Медианное кол-во посадочных мест')
fig.set_figheight(6)
fig.set_figwidth(10)
plt.show()
Конвеерное питание в школьных и заводских столовых объяснимо. Но в ресторанах зачем такая большая посадка? Или недавно почивший "МакДоналдс", прикидывающийся рестораном, портит картину?
#Разбиваем адрес на части в отдельном датафреме
adr_spl=fastfood['address'].str.split(pat=', ', expand=True)
#В городах-спутниках улица на третьей позиции, поэтому перемещаем на вторую
adr_spl.loc[adr_spl[1].str.contains('город|поселение|поселок', case=False),1] = adr_spl[2]
#Перемещаем улицу на первую позицию, кроме тех строк, где адрес уже начинался с улицы
adr_spl.loc[adr_spl[0].isin(['город Москва']),0] = adr_spl[1]
#Индексы таблиц одинаковы, поэтому просто добавляем столбец в основной датафрейм
fastfood['street']= adr_spl[0]
top10 = fastfood.groupby('street').count().sort_values(by='id', ascending=False)[0:10]
fig, axes = plt.subplots()
axes = sns.barplot(y=top10.index, x=top10['id'], palette='dark:#58C')
axes.set_title('Самые популярные улицы')
axes.set_ylabel('')
axes.set_xlabel('Количество объектов')
fig.set_figheight(6)
fig.set_figwidth(10)
plt.show()
Разумеется, самые длинные радиальные магистрали города попали в ТОП. И каждая из них расположена сразу в нескольких районах.
Пользуемся и забираем данные
#Внешние данные взяты лично мной с Портала открытых данных: https://data.mos.ru/opendata/.
spreadsheet_id = '12w7_M9ViLXXYfzQ7hMLqB-40foKu0Uhg1rqodcslOW8'
file_name = 'https://docs.google.com/spreadsheets/d/{}/export?format=csv'.format(spreadsheet_id)
r = requests.get(file_name)
dist_geo = pd.read_csv(BytesIO(r.content))
dist_geo.head(5)
| ID | Name | global_id | IsNetObject | OperatingCompany | TypeObject | AdmArea | District | Address | PublicPhone | ... | AdmArea_en | District_en | Address_en | PublicPhone_en | SeatsCount_en | SocialPrivileges_en | Longitude_WGS84_en | Latitude_WGS84_en | geodata_center | geoarea | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Код | Наименование | global_id | Является сетевым | Название управляющей компании | Вид объекта | Административный округ по адресу | Район | Адрес | Контактный телефон | ... | AdmArea_en | District_en | Address_en | PublicPhone_en | SeatsCount_en | SocialPrivileges_en | Longitude_WGS84_en | Latitude_WGS84_en | geodata_center | geoarea |
| 1 | 00151635 | СМЕТАНА | 637376221 | нет | NaN | кафе | Северо-Восточный административный округ | Ярославский район | город Москва, улица Егора Абакумова, дом 9 | PublicPhone:(499) 183-14-10\n\n | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 2 | 000077874 | Родник | 637376331 | нет | NaN | кафе | Центральный административный округ | Таганский район | город Москва, улица Талалихина, дом 2/1, корпус 1 | PublicPhone:(495) 676-55-35\n\n | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 3 | 000024309 | Кафе «Академия» | 637376349 | нет | NaN | ресторан | Центральный административный округ | Таганский район | Российская Федерация, город Москва, внутригоро... | PublicPhone:(495) 662-30-10\n\n | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 4 | 000027429 | ГБОУ «Школа № 1430 имени Героя Социалистическо... | 637376480 | нет | NaN | столовая | Северо-Восточный административный округ | район Лианозово | город Москва, Угличская улица, дом 17 | PublicPhone:(499) 908-06-15\n\n | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
5 rows × 29 columns
Оставим только нужные столбцы, избавимся от первой строки и посмотрим дубликаты адресов
dist_geo = dist_geo[['AdmArea','District','Address','Longitude_WGS84','Latitude_WGS84']][1:]
print('Количество дубликатов в адресах:', dist_geo[['Address']].duplicated().sum())
Количество дубликатов в адресах: 7716
Из внешних данных нужна только привязка района и геопозиции к адресу, поэтому дубликаты адресов удаляем.
dist_geo = dist_geo[~dist_geo[['Address']].duplicated()].reset_index(drop=True)
Присоединяем нужные данные к нашему датасету
fastfood_upd = fastfood.merge(dist_geo, left_on=['address'], right_on=['Address'], how='left')
fastfood_upd.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 15140 entries, 0 to 15139 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 15140 non-null int64 1 object_name 15140 non-null object 2 chain 15140 non-null bool 3 object_type 15140 non-null object 4 address 15140 non-null object 5 number 15140 non-null int64 6 street 15140 non-null object 7 AdmArea 11893 non-null object 8 District 11893 non-null object 9 Address 11893 non-null object 10 Longitude_WGS84 11893 non-null object 11 Latitude_WGS84 11893 non-null object dtypes: bool(1), int64(2), object(9) memory usage: 1.4+ MB
К сожалению, больше 20% адресов не привязались. Заменим пустые районы понятным значением.
fastfood_upd['District'] = fastfood_upd['District'].fillna('Не найдено')
fastfood_upd['AdmArea'] = fastfood_upd['AdmArea'].fillna('Не найдено')
#Удаляем дублирующий примердженный столбец
fastfood_upd.drop(columns=['Address'], inplace=True)
Посмотрим распределение ТОП-10 улиц по районам и округам
distr_top10 = fastfood_upd.query('street in @top10.index').groupby('District').agg(
{'AdmArea':'first', 'id':'count'})
display(distr_top10.sort_values(by='id', ascending=False).head(10))
print('Кол-во районов :', len(distr_top10))
| AdmArea | id | |
|---|---|---|
| District | ||
| Не найдено | Не найдено | 213 |
| Пресненский район | Центральный административный округ | 164 |
| район Чертаново Центральное | Южный административный округ | 82 |
| район Тропарёво-Никулино | Западный административный округ | 70 |
| район Аэропорт | Северный административный округ | 61 |
| Обручевский район | Юго-Западный административный округ | 59 |
| район Свиблово | Северо-Восточный административный округ | 55 |
| Мещанский район | Центральный административный округ | 53 |
| район Беговой | Северный административный округ | 45 |
| район Чертаново Южное | Южный административный округ | 44 |
Кол-во районов : 39
Странный рейтинг районов. Не найденные районы должны были распределиться равномерно. Нецентровые улицы, очевидно, дают малый вклад в самые насыщенные центральные районы. А если сравнить с рейтингом по всем улицам?
distr_all = fastfood_upd.groupby('District').agg({'AdmArea':'first', 'id':'count'})
display(distr_all.sort_values(by='id', ascending=False).head(10))
| AdmArea | id | |
|---|---|---|
| District | ||
| Не найдено | Не найдено | 3247 |
| Тверской район | Центральный административный округ | 660 |
| Пресненский район | Центральный административный округ | 612 |
| Басманный район | Центральный административный округ | 521 |
| Даниловский район | Южный административный округ | 357 |
| район Замоскворечье | Центральный административный округ | 322 |
| Мещанский район | Центральный административный округ | 314 |
| район Хамовники | Центральный административный округ | 286 |
| Таганский район | Центральный административный округ | 261 |
| район Арбат | Центральный административный округ | 226 |
А это уже больше на правду похоже. Зато какие отличия! Попробуем визуализировать отличия обоих рейтингов.
#Объединяем рейтинги по району
top_diff = distr_top10.merge(distr_all, on='District', how='outer')
top_diff.drop(columns=['AdmArea_x'], inplace=True)
top_diff.columns = ['top10_prct','admarea', 'full_prct']
#Удаляем из рейтинга район "Не найдено"
top_diff = top_diff.query('admarea != "Не найдено"')
#Районам, не вошедшим в distr_top10, присваивам минмальное значение, чтобы они попали на график.
top_diff = top_diff.fillna(0.1)
#Вычисляем весовые доли районов
top_diff['top10_prct'] = top_diff['top10_prct']/top_diff['top10_prct'].sum()*100
top_diff['full_prct'] = top_diff['full_prct']/top_diff['full_prct'].sum()*100
#Строим интерактивный график с подписями округов
fig = px.scatter(
top_diff, x='full_prct', y='top10_prct', size='full_prct',
hover_name=top_diff.index, hover_data=['admarea']
)
fig.update_layout(height=700, width=800, title_text='Взаимная зависимость доли района в общем количестве объектов, %')
fig.show()
#Вывод картинки plotly без Jupyter
plt.figure(figsize=(12,8))
img = plt.imread('C:\Марк\ЯП\DA\Проекты\pr9\scatter_px.png')
plt.xticks(visible=False)
plt.yticks(visible=False)
plt.imshow(img)
plt.show()
Корреляция по районам незначительная. Ну, не пролегают длинные улицы в самых топовых на общепит районах. Только 2 района из ТОП-10 совпадают и оба в ЦАО. А по округам корреляция лучше.
Если отвечать на вопрос про ТОП-5 районов для ТОП-10 самых длинных улиц, то туда и Свиблово попадает.
Если же учитывать насыщенность общепитом по всем улицам, то скорее правильно определиться с округами.
Итого, самым насыщенным является
Пресненский и Мещанский районы), а такжеТропарёво-Никулино, Дорогомилово),Чертаново всякое) иАэропорт, Беговая).ll = fastfood_upd.groupby('street').agg(
{'AdmArea':'first', 'id':'count'}).query('id < 2')
print('Улиц с одним объектом -', len(ll))
ll.head(10)
Улиц с одним объектом - 714
| AdmArea | id | |
|---|---|---|
| street | ||
| 1-й Балтийский переулок | Северный административный округ | 1 |
| 1-й Басманный переулок | Центральный административный округ | 1 |
| 1-й Ботанический проезд | Северо-Восточный административный округ | 1 |
| 1-й Вешняковский проезд | Не найдено | 1 |
| 1-й Голутвинский переулок | Центральный административный округ | 1 |
| 1-й Заречный переулок | Троицкий административный округ | 1 |
| 1-й Зачатьевский переулок | Не найдено | 1 |
| 1-й Кирпичный переулок | Не найдено | 1 |
| 1-й Кожевнический переулок | Не найдено | 1 |
| 1-й Кожуховский проезд | Не найдено | 1 |
Больше 700 улиц - по сути весь город.
Внешние данные уже загружены для всего датафрейма, поэтому сразу смотрим в каких районах больше улиц с одной едальней?
kk = fastfood_upd.query('street in @ll.index').groupby('District').agg(
{'AdmArea':'first', 'id':'count'})
display(kk.sort_values(by='id', ascending=False).head(10))
print('Кол-во районов :', len(kk))
| AdmArea | id | |
|---|---|---|
| District | ||
| Не найдено | Не найдено | 243 |
| район Крюково | Зеленоградский административный округ | 28 |
| Таганский район | Центральный административный округ | 22 |
| Пресненский район | Центральный административный округ | 18 |
| Басманный район | Центральный административный округ | 17 |
| район Савёлки | Зеленоградский административный округ | 16 |
| район Хамовники | Центральный административный округ | 15 |
| Тверской район | Центральный административный округ | 15 |
| район Старое Крюково | Зеленоградский административный округ | 13 |
| Нижегородский район | Юго-Восточный административный округ | 12 |
Кол-во районов : 109
В центре много мелких улиц, в Зеленограде улицы-корпуса, ну и малонаселенные окраины до кучи. А в целом, весь город - больше 100 районов.
Смотрим распределение вместимости в самых насыщенных районах.
#Ограничим список 20-тью районами
mm = fastfood_upd.groupby('District').agg({'id':'count'}).sort_values(by='id', ascending=False)[0:20]
nn = fastfood_upd.query('District in @mm.index and AdmArea != "Не найдено"')
fig, ax = plt.subplots()
fig.set_figheight(8)
fig.set_figwidth(12)
sns.stripplot(y='District', x='number', data=nn, jitter=0.15, size=3)
plt.title('Разброс количества посадочных мест по районам')
plt.xlabel('Посадочные места');
#Выбросы не покажем
ax.set_xlim(0,400);
Красиво, видны отсечки 50, 100 и 150 мест, но не очень понятна закономерность. Посмотрим иначе:
#Этот график более информативный
fig, ax = plt.subplots()
fig.set_figheight(8)
fig.set_figwidth(12)
sns.boxplot(y='District', x='number', data=nn, linewidth=1, color='#5c86bb', fliersize=3)
ax.set_xlim(0,400);
В отдаленных районах больше чем в центре доля заведений с большим количеством посадочных мест (возможно это школьные столовые?), но при этом медианная вместимость в центральных районах немного выше.
И, наконец, можно посмотреть на карту расположения самых насыщенных общепитом мест Москвы
map_center = {"lon": 37.6214, "lat": 55.7547}
fig = px.scatter_mapbox(nn, lon='Longitude_WGS84', lat='Latitude_WGS84',
mapbox_style='open-street-map', center=map_center, zoom=11,
hover_name='object_name', size='number', size_max=25,
color='chain', hover_data=['District', 'AdmArea'])
fig.update_traces(showlegend=True)
fig.update_layout(height=1200, width=900)
fig.show()
#Вывод картинки plotly без Jupyter
plt.figure(figsize=(24,18))
img = plt.imread('C:\Марк\ЯП\DA\Проекты\pr9\map_px2.png')
plt.xticks(visible=False)
plt.yticks(visible=False)
plt.imshow(img)
plt.show()
Для открытия предпочтительно кафе в районе Москва-Сити вместимостью до 40 п.м. Расширяться желательно, меняя формат на ресторан и увеличивая число посадочных мест. Для развития сети следует обратить внимание на спальники в ЗАО, ЮАО и южную часть САО, сохраняя вместимость до 50 п.м.